#!/usr/bin/python
# coding=utf-8

import commands
from addon_manager.addon_exception import GetUseablePortListFailed, AllocatePortFailed, BuiltinAddOnDuplicatedPort
from addon_manager.addon_logger import AddOnLogger
from addon_manager.addon_whitelist import AddOnWhitelist


class AddOnPortMgr(object):
    __UNUSED = 0
    __ASSIGNED = 1
    __USED = 2
    __OCCUPIED = 3

    __INST = None

    @staticmethod
    def get_inst(config):
        if AddOnPortMgr.__INST is None:
            AddOnPortMgr.__INST = AddOnPortMgr(config)
        return AddOnPortMgr.__INST

    def __init__(self, config):
        self.config = config
        """
            useable_port_list: 均为长度为3的列表，0表示端口，1表示当前状态，2表示预分配addon名称
            TODO: 在端口未使用时，避免端口被随机分配给其它的进程
        """
        self.useable_port_list = [[i, self.__UNUSED, None] for i in range(self.config.port_start, self.config.port_end)]
        self.port_map = {item[0]: item for item in self.useable_port_list}

        addon_whitelist = AddOnWhitelist.inst()
        name_port_pairs = addon_whitelist.get_addon_name_port_pairs()
        for name, port in name_port_pairs:
            tgt = self.port_map.get(port)
            if tgt is None:
                self.useable_port_list.append([port, self.__ASSIGNED, name])
            elif tgt[1] == self.__UNUSED:
                tgt[1] = self.__ASSIGNED
                tgt[2] = name
            else:
                raise BuiltinAddOnDuplicatedPort(port)

    def __get_occupied_port_list(self):
        # ss (https://www.linuxcool.com/ss): 输出去掉表头的所有的ipv4 tcp连接
        # awk \'{print $4}\': 截取ss localaddr所在的一列(即第四列)
        # awk -F \':\' \'{print $NF}\': 以:为分隔符拆分ip:port, :::ssh并获取最后一列，即端口号所在列，或ssh(22), http(80), https(403)字符串
        cmd = "ss -t -a -H -4 -n | awk '{print $4}' | awk -F ':' '{print $NF}'"
        status, output = commands.getstatusoutput(cmd)
        if status != 0:
            raise GetUseablePortListFailed()

        used_portlist = []
        port_str_list = output.split("\n")
        for port_str in port_str_list:
            try:
                port = int(port_str)
                if self.config.port_end > port >= self.config.port_start:
                    used_portlist.append(port)
            except ValueError:
                AddOnLogger().warn('cannot convert str "%s" to int', port_str)

        return used_portlist

    def __try_change_to_occupied(self, port_info):
        if port_info[1] == self.__UNUSED:
            port_info[1] = self.__OCCUPIED

    def __try_change_to_unused(self, port_info):
        if port_info[1] == self.__OCCUPIED:
            port_info[1] = self.__UNUSED

    def __try_change_to_assigned(self, port_info):
        if port_info[1] == self.__USED:
            port_info[1] = self.__ASSIGNED

    def __try_change_to_used(self, port_info):
        ret = False
        if port_info[1] == self.__ASSIGNED:
            port_info[1] = self.__USED
            ret = True
        return ret

    def update_useable_port_list(self):
        occupied_list = self.__get_occupied_port_list()
        for port in self.useable_port_list:
            if port[0] in occupied_list:
                self.__try_change_to_occupied(port)
            else:
                self.__try_change_to_unused(port)

    def __get_allocate_port(self, addon_filename):
        ret = None
        for port_info in self.useable_port_list:
            if port_info[2] == addon_filename:
                ret = port_info
                break
        return ret

    def __find_first_unused_port(self):
        self.useable_port_list.sort(key=lambda x: (x[1], x[0]))
        if self.useable_port_list[0][1] == self.__UNUSED:
            return self.useable_port_list[0]
        else:
            raise AllocatePortFailed()

    def allocate_port(self, addon_filename):
        port_info = self.__get_allocate_port(addon_filename)
        if port_info is not None:
            return port_info[0]

        self.update_useable_port_list()

        alloc_port = self.__find_first_unused_port()
        alloc_port[1] = self.__ASSIGNED
        alloc_port[2] = addon_filename
        # AddOnLogger().debug('alloc_port %s', self.useable_port_list)

        return alloc_port[0]

    def release_port(self, addon_filename):
        port_info = self.__get_allocate_port(addon_filename)
        if port_info is None:
            return True

        port_info[1] = self.__UNUSED
        port_info[2] = None
        return True

    def start_use_port(self, addon_filename):
        port_info = self.__get_allocate_port(addon_filename)
        if port_info is None:
            return None
        if self.__try_change_to_used(port_info) == True:
            return port_info[0]
        else:
            return None

    def stop_use_port(self, addon_filename):
        port_info = self.__get_allocate_port(addon_filename)
        if port_info is None:
            return False
        self.__try_change_to_assigned(port_info)
        return True

    def find_useable_port(self):
        return self.__get_occupied_port_list()


__all__ = ["AddOnPortMgr"]

if __name__ == "__main__":
    from addon_manager.addon_config import AddOnConfig

    def Test_AddOnPortMgr():
        addon_port_mgr = AddOnPortMgr(AddOnConfig.inst())
        print("addon_port_mgr {}".format(addon_port_mgr.useable_port_list))
        print(addon_port_mgr.find_useable_port())
